package main

import (
    "crypto/md5"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "sync"
    "strings"
)

/*
   must be thread safe
*/
type Auth interface {
    AuthUserPass(string, string, bool) bool //user pass
    ChangePassTo(string, string) error      //user pass
    AuthKey(string, string, string) bool    //user addr key
    AddKey(string, string, string) error    //user addr key
    Reload() error
    Unload() error
}

type Secret struct {
    Salt   string `json:"salt"`
    Shadow string `json:"shadow"`
}

type LocalAuth struct {
    mu         sync.RWMutex
    secretFile string
    secrets    map[string]*Secret
    keyFile    string
    keys       map[string]map[string]string //user ip key
}

func NewLocalAuth(secret,key string) (*LocalAuth, error) {
    auth := &LocalAuth{secretFile: secret,keyFile:key}
    if err := auth.Reload(); err != nil {
        return nil, err
    }
    return auth, nil
}

func (ja *LocalAuth) ChangePassTo(uname, passwd string) error {
    if passwd == "" {
        return fmt.Errorf("null password")
    }
    ja.mu.RLock()
    defer ja.mu.RUnlock()
    i, ok := ja.secrets[uname]
    if !ok {
        return fmt.Errorf("unknown user")
    }
    i.Shadow = salted(passwd, i.Salt)
    return nil
}

func (ja *LocalAuth) Unload() error {
    ja.mu.RLock()
    defer ja.mu.RUnlock()
    b, err := json.Marshal(ja.secrets)
    if err != nil {
        return err
    }
    if err = ioutil.WriteFile(ja.secretFile, b, 0600); err != nil {
        return err
    }
    if b, err = json.Marshal(ja.keys); err != nil {
        return err
    }
    return ioutil.WriteFile(ja.keyFile, b, 0600)
}

func (ja *LocalAuth) AddKey(uname, addr, key string) error {
    ja.mu.Lock()
    defer ja.mu.Unlock()
    _, ok := ja.secrets[uname]
    if !ok {
        return fmt.Errorf("unknown user")
    }

    if _,ok:=ja.keys[uname];!ok{
        ja.keys[uname]=make(map[string]string)
    }
    ja.keys[uname][choped(addr)] = key
    return nil
}

func choped(addr string) string {
    if strings.Contains(addr, ":") {
        s := strings.Split(addr, ":")
        return s[0]
    }
    return addr
}


func (ja *LocalAuth) AuthKey(uname, addr, key string) bool {
    ja.mu.RLock()
    defer ja.mu.RUnlock()
    if au, ok := ja.keys[uname]; ok {
        if k, ok := au[choped(addr)]; ok {
            return k == key
        }
    }
    return false
}

func salted(pass, salt string) string {
    md5sum := md5.Sum([]byte(pass + salt))
    return fmt.Sprintf("%x", md5sum)
}

func (ja *LocalAuth) AuthUserPass(uname, passwd string, isSalted bool) bool {
    ja.mu.RLock()
    defer ja.mu.RUnlock()
    if au, ok := ja.secrets[uname]; ok {
        if !isSalted {
            return salted(passwd, au.Salt) == au.Shadow
        }
        return passwd == au.Shadow
    }
    return false
}

func (ja *LocalAuth) Reload() error {
    ja.mu.Lock()
    defer ja.mu.Unlock()
    b, err := ioutil.ReadFile(ja.secretFile)
    if err != nil {
        return fmt.Errorf("secrets file, %v",err)
    }
    if err = json.Unmarshal(b, &ja.secrets); err != nil {
        return fmt.Errorf("secrets file, %v",err)
    }
    if b, err = ioutil.ReadFile(ja.keyFile); err != nil {
        return fmt.Errorf("key file, %v",err)
    }
    if err= json.Unmarshal(b, &ja.keys);err!=nil{
        return fmt.Errorf("key file, %v",err)
    }
    for username:=range ja.keys{
        if _,ok:=ja.secrets[username];!ok{
            return fmt.Errorf("Key %s not in secrets",username)
        }

    }
    return nil
}
